

package com.googlecode.flac4j.format.frame;


import com.googlecode.flac4j.format.Stream;
import com.googlecode.flac4j.format.frame.sub.SubFrame;
import com.googlecode.flac4j.format.util.CRC16;

import com.googlecode.jinahya.io.BitInput;
import com.googlecode.jinahya.io.BitOutput;
import com.googlecode.jinahya.util.BitAccessible;

import java.io.IOException;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import java.util.zip.Checksum;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;


/**
 *
 * @author Jin Kwon <jinahya at gmail.com>
 */
@XmlType(propOrder = {"header", "subFrames"})
public class Frame implements BitAccessible {


    @Override
    public void read(final BitInput input) throws IOException {

        assert input.available() == 0;

        final Checksum checksum = new CRC16();
        input.addChecksum(checksum);
        try {
            readContent(input);
            assert checksum.getValue() == input.readUnsignedLong(16);
        } finally {
            input.removeChecksum(checksum);
        }
    }


    /**
     * 
     * @param input
     * @throws IOException 
     */
    protected void readContent(final BitInput input) throws IOException {

        getHeader().read(input);

        int channelCount = 0;
        switch (header.getChannelAssignment()) {
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
                channelCount = header.getChannelAssignment() + 1;
                break;
            case 0x08: // 1000
            case 0x09: // 1001
            case 0x0A: // 1010
                channelCount = 2;
                break;
            default:
                break;
        }

        subFrames.clear();
        for (int i = 0; i < channelCount; i++) {
            final SubFrame subFrame = new SubFrame();
            subFrame.setFrame(this);
            subFrame.read(input);
            addSubFrame(subFrame);
        }

        input.align(1); // zero-padding to byte alignment
    }


    @Override
    public void write(final BitOutput output) throws IOException {

        assert output.available() == 0;

        final Checksum checksum = new CRC16();
        output.addChecksum(checksum);
        try {
            writeContent(output);
            output.writeUnsignedLong(16, checksum.getValue());
        } finally {
            output.removeChecksum(checksum);
        }
    }


    /**
     * 
     * @param output
     * @throws IOException 
     */
    protected void writeContent(final BitOutput output) throws IOException {

        header.write(output);

        switch (header.getChannelAssignment()) {
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
                if (subFrames.size() != header.getChannelAssignment() + 1) {
                    throw new IllegalStateException(
                        "subFrames.size(" + subFrames.size()
                        + ") != header.channelAssignment + 1");
                }
                break;
            case 0x08: // 1000
            case 0x09: // 1001
            case 0x0A: // 1010
                if (subFrames.size() != 2) {
                    throw new IllegalStateException(
                        "subFrames.size(" + subFrames.size() + ") != 2");
                }
                break;
            default:
                break;
        }

        for (SubFrame subFrame : subFrames) {
            subFrame.write(output);
        }

        output.align(1);
    }


    // ----------------------------------------------------------- parent stream
    /**
     * Returns the parent stream of this frame.
     *
     * @return parent stream.
     */
    public Stream getStream() {
        return stream;
    }


    /**
     * Sets the parent stream of this frame.
     *
     * @param stream parent stream
     */
    public void setStream(final Stream stream) {

        if (stream == null) {
            throw new NullPointerException("null stream");
        }

        this.stream = stream;
    }


    // ------------------------------------------------------------------ header
    /**
     * Returns header of this frame.
     *
     * @return header
     */
    public Header getHeader() {

        if (header == null) {
            header = new Header();
            header.setFrame(this);
        }

        return header;
    }


    // --------------------------------------------------------------- subFrames
    /**
     * Clears subFrames.
     */
    public void clearSubFrames() {
        subFrames.clear();
    }


    /**
     * Returns the number of SubFrames added so far.
     *
     * @return SubFrame count
     */
    public int getSubFrameCount() {
        return subFrames.size();
    }


    /**
     * Adds given <code>subFrame</code> to the list.
     *
     * @param subFrame subFrame to add
     */
    public void addSubFrame(final SubFrame subFrame) {

        if (subFrame == null) {
            throw new NullPointerException("null subFrame");
        }

        switch (header.getChannelAssignment()) {
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
                if (subFrames.size() == header.getChannelAssignment() + 1) {
                    throw new IllegalStateException("can't add more SubFrames");
                }
                break;
            case 0x08:
            case 0x09:
            case 0x0A:
                if (subFrames.size() == 2) {
                    throw new IllegalStateException("can't add more SubFrames");
                }
                break;
            default:
                break;
        }

        subFrame.setFrame(this);
        subFrames.add(subFrame);
    }


    /*
     * Adds given <code>subFrame</code> to the list at specified
     * <code>index</code>.
     *
     * @param index index
     * @param subFrame SubFrame to add
    public void addSubFrame(final int index, final SubFrame subFrame) {

        if (subFrame == null) {
            throw new NullPointerException("null subFrame");
        }

        subFrames.add(index, subFrame);
    }
     */


    /**
     * Returns an unmodifiable list of SubFrames added so far.
     *
     * @return an unmodifiable list of SubFrames added so far.
     */
    public List<SubFrame> getSubFrames() {
        return Collections.unmodifiableList(subFrames);
    }


    /** parent stream. */
    @XmlTransient
    private Stream stream;


    /** header. */
    @XmlElement(required = true)
    private Header header;


    /** subFrames. */
    @XmlElement(name = "subFrame")
    @XmlElementWrapper(required = true)
    private final List<SubFrame> subFrames = new ArrayList<SubFrame>();


}

